"""
Description: 五子棋 v2 版本，棋子使用黑白色的图标来绘制更加立体好看，并增加了背景图片、按钮和音效。
Version: 1.0
Author: marshal
Date: 2024-06-03 22:05 
LastEditors: marshal
LastEditTime: 2024-06-03 22:05 
"""

import pygame as pg
import sys
from pygame.locals import *
import numpy as np

pg.init()
# 颜色
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
TURQUOISE3 = (0, 197, 205)
# 棋盘线条颜色
LINE_COLOR = BLACK
# 棋盘参数
MARGINS = 50  # 边距
SPACING = 40  # 格子间距
LINES = 15  # 线条数
# 屏幕尺寸
SCREEN_HEIGHT = MARGINS * 2 + (LINES - 1) * SPACING
SCREEN_WIDTH = MARGINS * 2 + (LINES - 1) * SPACING + 150
# 棋子半径
CHESS_RADIUS = 18
# 棋子状态
EMPTY = 0  # 空
BLACK_CHESS = 1  # 黑棋
WHITE_CHESS = 2  # 白棋
# 记录棋子状态，数据类型是二维数组
CHESS = np.zeros([15, 15], dtype=int)
# 标记点位
MARK_POINTS = [(3, 3), (3, 11), (7, 7), (11, 3), (11, 11)]
# 窗口基础设置
pg.display.set_caption("五子棋")
screen = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# 背景图片
background = pg.image.load("../pictures/bg.jpg")
# 棋子图片
white_chess_img = pg.image.load("../pictures/white_chess.png")
black_chess_img = pg.image.load("../pictures/black_chess.png")
white_chess_img_convert = white_chess_img.convert_alpha()
black_chess_img_convert = black_chess_img.convert_alpha()
# 赢了以后标记图
win_flag = pg.image.load("../pictures/flag_red.png")
# 字体
font = pg.font.Font("../fonts/SourceHanSansSC-Medium-2.otf", 20)
# 按钮设置
button_color = TURQUOISE3
button_reset_rect = pg.Rect(SCREEN_WIDTH - 150, (SCREEN_HEIGHT / 2) - 75, 100, 50)
button_reset_text = font.render("重新开始", True, BLACK)
button_exit_rect = pg.Rect(SCREEN_WIDTH - 150, (SCREEN_HEIGHT / 2) + 25, 100, 50)
button_exit_text = font.render("退出游戏", True, BLACK)
# 音效
chess_sound = pg.mixer.Sound("../sound/chess.mp3")
success_sound = pg.mixer.Sound("../sound/success.mp3")
mouse_click_sound = pg.mixer.Sound("../sound/mouse_click.mp3")
# 坐标偏移量
offsets = [(1, 0), (0, 1), (1, 1), (1, -1)]


def get_board_pos(x, y):
    """
    坐标转换：将 Pygame 屏幕的坐标转换为棋盘坐标
    :param x: Pygame 坐标
    :param y: Pygame 坐标
    :return:
    """
    if check_pos(x, y):
        row = round((x - MARGINS) / SPACING)
        col = round((y - MARGINS) / SPACING)
        return row, col


def draw_board(margins, spacing):
    """
    绘制棋盘，通常五子棋棋盘的尺寸是 15 X 15
    :param margins: 棋盘边距
    :param spacing: 每个格子的距离
    :return:
    """
    # 设置背景
    screen.blit(background, (0, 0))
    # 绘制按钮
    pg.draw.rect(screen, button_color, button_reset_rect)
    screen.blit(button_reset_text, (button_reset_rect.centerx - button_reset_text.get_width() / 2,
                                    button_reset_rect.centery - button_reset_text.get_height() / 2))
    pg.draw.rect(screen, button_color, button_exit_rect)
    screen.blit(button_exit_text, (button_exit_rect.centerx - button_exit_text.get_width() / 2,
                                   button_exit_rect.centery - button_exit_text.get_height() / 2))
    # 先画外侧线
    pg.draw.lines(screen, LINE_COLOR, True,
                  [(margins, margins), (margins, SCREEN_HEIGHT - margins),
                   (SCREEN_HEIGHT - margins, SCREEN_HEIGHT - margins),
                   (SCREEN_HEIGHT - margins, margins)], 4)
    # 画内侧线
    for i in range(1, LINES - 1):
        # 画竖线
        pg.draw.line(screen, LINE_COLOR, (margins + spacing * i, margins),
                     (margins + spacing * i, SCREEN_HEIGHT - margins), 2)
        # 画横线
        pg.draw.line(screen, LINE_COLOR, (margins, margins + spacing * i),
                     (SCREEN_HEIGHT - margins, margins + spacing * i), 2)

    # 标记特殊点位
    for points in MARK_POINTS:
        pos_x = points[0]
        pos_y = points[1]
        radius = 7 if pos_x == int(LINES / 2) else 5
        pg.draw.circle(screen, LINE_COLOR, (margins + spacing * pos_x, margins + spacing * pos_y), radius)


def check_pos(x, y):
    """
    判断坐标是否在棋盘内
    :param x: 横坐标
    :param y: 纵坐标
    :return:
    """
    return MARGINS <= x <= SCREEN_HEIGHT - MARGINS and MARGINS <= y <= SCREEN_HEIGHT - MARGINS


def play(x, y, color):
    """
    下棋方法，实现思路是记录鼠标左键点击的坐标的颜色
    :return:
    """
    if check_pos(x, y):
        row, col = get_board_pos(x, y)
        # 如果该点位的状态是 EMPTY ，说明该位置为没有落子，则可以设置颜色
        if CHESS[row][col] == EMPTY:
            chess_status = BLACK_CHESS if color == BLACK else WHITE_CHESS
            CHESS[row][col] = chess_status
            # 播放下棋音效
            chess_sound.play()
            # 落子成功后要判断是否赢了
            status, pos_list = is_win(x, y, chess_status)
            if status != EMPTY:
                # 播放success音效
                success_sound.play()
                # 标注五子连珠
                draw_win_chess(pos_list)
                # 提示语
                if status == BLACK_CHESS:
                    text = font.render("黑棋赢了，将在5秒后重新开始", True, BLACK)
                else:
                    text = font.render("白棋赢了，将在5秒后重新开始", True, WHITE)
                text_pos = text.get_rect(centerx=SCREEN_HEIGHT / 2, y=10)
                screen.blit(text, text_pos)
                pg.display.update()
                pg.time.delay(5000)
                # 重置棋盘
                reset_board()
            else:
                if status == BLACK_CHESS:
                    text = font.render("黑棋回合", True, BLACK)
                else:
                    text = font.render("白棋回合", True, WHITE)
                text_pos = text.get_rect(centerx=SCREEN_HEIGHT / 2, y=10)
                screen.blit(text, text_pos)
    return True


def is_win(mouse_x, mouse_y, value):
    """
    判断输赢
    :param mouse_x:  鼠标 X 坐标
    :param mouse_y:  鼠标 Y 坐标
    :param value:  当前坐标的值，即黑子还是白子
    :return:
    """
    x, y = get_board_pos(mouse_x, mouse_y)
    for offset in offsets:
        count = 1
        # 记录五子连珠的坐标
        win_pos_list = [(x, y)]
        x_offset = offset[0]
        y_offset = offset[1]
        for step in range(1, 5):
            _x = x + step * x_offset
            _y = y + step * y_offset
            if 0 <= _x < LINES and 0 <= _y < LINES and CHESS[_x][_y] == value:
                win_pos_list.append((_x, _y))
                count += 1
            else:
                break
        for step in range(1, 5):
            _x = x - step * x_offset
            _y = y - step * y_offset
            if 0 <= _x < LINES and 0 <= _y < LINES and CHESS[_x][_y] == value:
                win_pos_list.append((_x, _y))
                count += 1
            else:
                break
        if count >= 5:
            return value, win_pos_list
    return EMPTY, []


def draw_win_chess(pos_list):
    """
    对五子连珠的棋子进行标注
    :param pos_list: 五子连珠的坐标
    :return:
    """
    for pos in pos_list:
        row, col = pos[0], pos[1]
        win_flag_img = win_flag.convert_alpha()
        win_flag_rect = win_flag_img.get_rect()
        win_flag_rect.center = (MARGINS + SPACING * row, MARGINS + SPACING * col)
        screen.blit(win_flag_img, win_flag_rect)


def reset_board():
    """
    重置棋盘
    :return:
    """
    global CHESS
    CHESS = np.zeros([15, 15], dtype=int)
    pg.display.update()


def draw_chess():
    """
    绘制棋子
    :return:
    """
    # 如果存在非 0 的数值则说明不是空棋盘
    if np.any(CHESS > 0):
        for row in range(LINES):
            for col in range(LINES):
                status = CHESS[row][col]
                if status != EMPTY:
                    img = black_chess_img if status == BLACK_CHESS else white_chess_img
                    img_pos = img.get_rect()
                    img_pos.center = (MARGINS + SPACING * row, MARGINS + SPACING * col)
                    screen.blit(img, img_pos)


def draw_preset_chess(x, y, color):
    """
    画出预设棋子，当光标移动到某一个棋盘点位时，显示一个透明度较高的棋子，展示当前应该落什么颜色的棋子
    :param x: 横坐标
    :param y: 纵坐标
    :param color: 棋子颜色
    """
    if check_pos(x, y):
        row, col = get_board_pos(x, y)
        pos = (MARGINS + SPACING * row, MARGINS + SPACING * col)
        img = black_chess_img_convert if color == BLACK else white_chess_img_convert
        # 设置透明度
        img.set_alpha(180)
        img_pos = img.get_rect()
        img_pos.center = pos
        screen.blit(img, img_pos)


def main():
    running = True
    is_black = True
    while running:
        # 检测退出事件
        for event in pg.event.get():
            if event.type == QUIT:
                running = False
            elif event.type == MOUSEBUTTONDOWN and event.button == 1:
                x, y = pg.mouse.get_pos()
                # 落子
                if play(x, y, BLACK if is_black else WHITE):
                    is_black = not is_black
                # 重新开始按钮
                if button_reset_rect.collidepoint(x, y):
                    mouse_click_sound.play()
                    pg.time.delay(500)
                    reset_board()
                # 退出按钮
                if button_exit_rect.collidepoint(x, y):
                    mouse_click_sound.play()
                    pg.time.delay(1000)
                    running = False
        # 绘制棋盘
        draw_board(MARGINS, SPACING)
        # 当鼠标移动到棋盘点位时，提示可以下什么颜色的棋子
        x, y = pg.mouse.get_pos()
        draw_preset_chess(x, y, BLACK if is_black else WHITE)
        # 绘制已经落下的棋子
        draw_chess()
        # 检查是否赢了
        pg.display.update()
    pg.quit()
    sys.exit()


if __name__ == '__main__':
    main()
